清洗後的圖檔,部分仍有紅框等雜訊,或是中文字體顏色不同(藍色、黑色),如下圖。
若將含有不同顏色中文字或不同位置紅框的圖檔納入模型訓練,可能影響後續模型的辨識效果。(如:同一個中文字,因字體顏色是黑色或藍色,導致辨識結果錯誤)
我們選擇以HSV進行顏色追蹤,在儘可能保留中文字字跡的前提下,利用opencv mask功能去除紅框。此外,將圖檔以灰階圖與二值化圖呈現,比較兩者效果。
最終選擇以灰階圖作為訓練樣本,並將資料集以7:3比例分配成train與test,供後續訓練模型之用。
HSV(Hue, Saturation, Value)
1.1 HSV
由色調(Hue)、飽和度(Saturation)、亮度(Value)三個分量組成,顏色分佈如下。
圖片來自於:https://www.itread01.com/content/1549945446.html
HSV顏色空間,能更直觀的表示人眼對色彩的感受。
基本上,HSV只要確定色調(1個分量),就可確定是何種顏色;一般的RGB,則是依照每Red、Green、Blue(3個分量)的比例,才能確定是何種顏色。
1.2 顏色追蹤(HSV+opencv)
# 將RGB轉換成HSV顏色空間
redhsv1 = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
HSV顏色追蹤與mask
2.1 HSV調色盤-查詢HSV值
import cv2
import numpy as np
# 讀取中文路徑圖檔(圖片讀取為BGR)
def cv_imread(filePath):
cv_img = cv2.imdecode(np.fromfile(filePath, dtype=np.uint8), -1)
return cv_img
# 點擊欲判定HSV值的圖片位置(以滑鼠左鍵單擊)
def mouse_click(event, x, y, flags, para):
if event == cv2.EVENT_LBUTTONDOWN:
print("BGR:", img[y, x])
print("GRAY:", gray[y, x])
print("HSV:", hsv[y, x])
print('='*30)
if __name__ == '__main__':
# 讀取圖檔
img = cv_imread('./data/04-清洗標籤後圖片/清洗標籤final/可用/2_惠.jpg')
img = cv2.resize(img, (320, 240))
# 轉換成gray與HSV
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
cv2.namedWindow("img")
cv2.setMouseCallback("img", mouse_click)
while True:
cv2.imshow('img', img)
if cv2.waitKey() == ord('q'):
break
cv2.destroyAllWindows()
效果
2.2 鎖定HSV閾值與Demo:載入圖檔後,藉由滑動滾輪調整HSV值,逐一挑選出紅框、藍色字體、黑色字體的HSV閾值,示意圖如下。
圖片來自於:https://blog.csdn.net/weixin_42216109/article/details/89520423
得到紅框、藍色字體、黑色字體HSV閾值後,開始去除雜訊,並比較灰階圖、二值化圖,程式碼如下。
3.1 追蹤紅框、藍色字體、黑色字體
import cv2
import numpy as np
import os
from PIL import Image
def red1_mask(img):
# 紅1
lower = np.array([150, 80, 94])
upper = np.array([180, 255, 255])
redhsv1 = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
mask1 = cv2.inRange(redhsv1, lower, upper)
return mask1
def red2_mask(img):
# 紅2
lower = np.array([0, 80, 89])
upper = np.array([10, 255, 255])
redhsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
mask = cv2.inRange(redhsv, lower, upper)
return mask
def blue_mask(img):
# 藍
lower = np.array([90, 43, 46])
upper = np.array([124, 255, 255])
redhsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
mask = cv2.inRange(redhsv, lower, upper)
return mask
def black_mask(img):
# 黑
lower = np.array([0, 0, 0])
upper = np.array([255, 255, 135])
redhsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
mask = cv2.inRange(redhsv, lower, upper)
return mask
3.2 設定膨脹函數
def my_dilate(img):
kernel = np.ones((3, 3), np.uint8)
new_img = cv2.dilate(img, kernel, iterations=1)
return new_img
3.3 找圖檔中的眾數,用來取代紅色的mask區域
def get_mode(img):
# 閾值取眾數
# bincount():統計非負整數的個數,不能統計浮點數
counts = np.bincount(img.flatten())
# counts的index代表出現的數,counts[index]代表出現數的次數
# 今要求counts[index] 排序後最大跟第二大的counts的index(代表眾數跟出現第二多次的數)
# 最後一個元素是counts最大值的index ,倒數第二是二大
counts_sort = np.argsort(counts)
index = counts_sort[-1]
# 以防圖片出現大量黑色面積
# 出現大量黑色區塊的話,取第二多數
if index <= 100:
index = counts_sort[-2]
return index
# 否則就return原本的眾數
return index
3.4 合併mask區域(刪去紅框、保留中文字)
# 合併mask區域
def process_img(img, turn=None):
# 紅1
mask1 = red1_mask(img)
# 紅2
mask2 = red2_mask(img)
# 合併紅1+紅2之範圍
mask3 = cv2.bitwise_or(mask1, mask2)
# 膨脹mask3
mask3 = my_dilate(mask3)
# 黑白反轉
mask3 = cv2.bitwise_not(mask3, mask3)
# 圖檔轉換成灰階
image = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 取眾數
image_mode =get_mode(image)
# 把紅色的mask區域換成眾數
image[mask3 == 0] = image_mode
# 顯示mask後圖片
image = cv2.resize(image, (160, 120))
cv2.imshow('mask', image)
cv2.waitKey()
# 高斯模糊後取眾數
blur = cv2.GaussianBlur(image, (3, 3), 0)
c = get_mode(blur) * 0.7
# 二值化
ret, thresh1 = cv2.threshold(image, c, 255, cv2.THRESH_BINARY)
if turn == 'binary':
thresh1 = cv2.resize(thresh1, (160, 120))
cv2.imshow('binary', thresh1)
cv2.waitKey()
return thresh1
if turn == 'gray':
blur = cv2.resize(blur, (160, 120))
cv2.imshow('gray', blur)
cv2.waitKey()
return blur
if __name__ == '__main__':
img = cv_imread('./data/04_清洗標籤後圖片/清洗標籤final/可用/2_惠.jpg')
# 顯示原圖片
img = cv2.resize(img, (160, 120))
cv2.imshow('origin', img)
cv2.waitKey()
process_img(img, 'binary')
process_img(img, 'gray')
成果展示
<原圖>
<灰階圖>
<二值化圖>
3.5 雖然將圖檔二值化,訓練模型效率較高,但可能造成圖像信息嚴重丟失;灰階則可以兼顧訓練效率與保留較多的梯度信息。因此,我們最終選擇以灰階圖作為訓練樣本。
分配trian與test資料集:先統計每個中文字的照片張數,再以7:3比例分配成train與test。
4.1 過程
import os
import random
import shutil
import os
import shutil
# 分成訓練集跟資料集
src_dir_name = './train/'
target_dir_name = './test/'
test_size = 0.3
labels = set(os.listdir(src_dir_name))
def word_classfier():
word_list_dir = []
for i in os.listdir(src_dir_name):
if i.endswith('.jpg'):
word_list_dir.append(i.split('.')[0][-1])
word_list_dir = set(word_list_dir)
print(word_list_dir)
for i in os.listdir(src_dir_name):
if i.endswith('.jpg'):
if i.split('.')[0][-1] in word_list_dir:
try:
os.mkdir(src_dir_name+i.split('.')[0][-1])
except FileExistsError:
pass
shutil.move(src_dir_name+i,src_dir_name+i.split('.')[0][-1]+'/'+i)
def move_test_data(test_data:list):
for i in test_data:
word_subfolder = i.split('.')[0][-1]
if word_subfolder in labels:
print(src_dir_name+word_subfolder+'/'+i)
try:
os.mkdir(target_dir_name +word_subfolder)
except FileExistsError:
pass
shutil.move(src_dir_name+word_subfolder+'/'+i,target_dir_name +word_subfolder+'/'+i)
elif word_subfolder not in labels:
word_subfolder = i.split('.')[0][0]
print(src_dir_name+word_subfolder+'/'+i)
try:
os.mkdir(target_dir_name +word_subfolder)
except FileExistsError:
pass
try:
shutil.move(src_dir_name+word_subfolder+'/'+i,target_dir_name +word_subfolder+'/'+i)
except FileNotFoundError:
shutil.move(src_dir_name + word_subfolder + '/' + i, target_dir_name + word_subfolder + '/' + i)
def test_train_split():
try:
os.mkdir(target_dir_name)
except FileExistsError:
pass
for i in os.listdir(src_dir_name):
#每個字的照片數
dir_length = len(os.listdir(src_dir_name+i))
#3:7抽樣
test_size = round(0.3 * dir_length)
test_data = random.sample(os.listdir(src_dir_name+i), k=test_size)
move_test_data(test_data)
if __name__ == '__main__':
# 把字分類成800個資料夾
word_classfier()
# 分成訓練集跟測試集
test_train_split()
4.2 成果
train與test
train資料夾內
讓我們繼續看下去...